// RemotePlayer.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include "sdlvlc.h"

using boost::asio::ip::udp;

namespace hinkle
{
    // The hinkle UDP protocol:
    // ************************
    //
    //  Handshake:
    //  ----------
    //   a) Remote client broadcasts a UDP DataGramCmd_RequestServerIP packet to port 4359, from port 4357. It listens on port 4357 for the reply.
    //      It will send the node address (value 1 to 10) inside the broadcast to the server, which will then remember it and associate it with 
    //      the endpoint.
    //   b) HLS Server replies with with DataGramCmd_RcvdServerIP and its IP Address to local port 4359, from port 4357.
    //   NOTE: The handshake is a bit broken. It prevents multiple clients from running on the same hardware and e.g. outputting video to different
    //   monitors.
    //   
    //  Test playing:
    //  -------------
    //   a) Server sends: HlsCmd2Player_LoadPlayFile to port 4311 with a filename. E.g.:
    //         "C:\Halloween.avi"
    //
    //  Show playing:
    //  -------------
    //   a) Server sends: HlsCmd2Player_FullScreen to port 4311 
    //   b) Server sends: HlsCmd2Player_ShowLoadWithTime to port 4311  with a cookie, "nomute" value and filename. E.g.:
    //         "12309,0,C:\Halloween.avi"
    //   c) Server waits amount of time specified in: "Output \ Manage Start of Sequence Video Player Trigger 
    //                                                 \ Number of MilliSeconds to Delay after Last Load to Play" 
    //      (In theory. In practice it only ever waits 500ms. The value can't be saved when configured - it can be configured per Sequence, but it will never be used. 
    //       It can also be configured for "Play Specific Play List" after you opened a PlayList, but then is useless since you can't play another PlayList 
    //       without restarting).
    //   d) Server sends: HlsCmd2Player_ShowPlayWithTime to port 4311 with the cookie from (c). E.g.:
    //        "12309"
    //
    //   Testing notes:
    //   --------------
    //   This is not a particularly robust protocol. There is a specific launching sequence between the 
    //   HLS editor and the remote player that needs to be adhered to. Specifically:
    //
    //     For "Test HLS Player":
    //      a) Open the HLS player
    //      b) Load a sequence in the HLS player.
    //      c) Open the Remote player
    //      d) Inside HLS, click on "Ouput \ Test HLS Player"
    //      e) Type in a filename at 'Video To Play at Player Selected'.
    //         NOTE: The filename must be 4 characters or longer. You will not get an error
    //               if the filename is shorter than 4 characters, but the packet won't be sent to the 
    //               remote player.
    //
    //     For "Play Specific Play List":
    //      a) Open the HLS player
    //      b) Click "Output\Play Specific Play List"
    //      c) Open the Remote player
    //      d) Inside HLS, click "SelectPlayList"
    //      e) Select the play list.
    //         NOTE: If you start the Remote Player before step (b), HLS will send HlsCmd2Player_ShowPlayWithTime commands but not HlsCmd2Player_ShowLoadWithTime 
    //         commands, with the result is the play will not work.
    //
    //  Dependencies.
    //  *************
    //
    //  This project depends on 3 libraries:
    //  a) LIBVLC 1.1.1
    //     https://wiki.videolan.org/LibVLC/
    //     See below for installation instructions
    //  b) BOOST 1.54
    //      http://boost.teeks99.com/
    //      Specifically: http://boost.teeks99.com/bin/1.54.0/boost_1_54_0-msvc-11.0-32.exe
    //  c) SDL 2.0
    //      http://www.libsdl.org/download-2.0.php
    //      Specifically:  http://www.libsdl.org/release/SDL2-2.0.0-win32-x86.zip
    //
    //  INSTALLING VLC DEPENDENCIES:
    //  ****************************
    //  To build this you will need a GIT enlistment as well as the VLC includes + libraries.
    //  On windows, follow these steps:
    //  a) Follow the GIT installation instructions on:
    //     https://wiki.videolan.org/Git_Windows/
    //  b) Create a folder on your machine (e.g. c:\vlc), right-click on it and click on "GIT Clone".  
    //     Make sure the "GIT Clone" has that directory specified - you don't need another subfolder.
    //  c) Use the following repository:
    //     git://git.videolan.org/vlc.git
    //  d) Right-click the RemotePlayer project, Click on Properties, and go to VC++ Directories. 
    //     Then add the VLC include path (e.g. C:\vlc\include) to "Include Directories", e.g.:
    //  e) There is a vlc.lib to link against as part of this project, but to create a new one, follow the steps on:
    //     https://wiki.videolan.org/GenerateLibFromDll/

    // The commands available. The names here matches the names from the original HLS_VideoPlayer source code for easier search.
    enum class command : uint16_t
    {
        DataGramCmd_LiveSound = 100,                    // 100
        DataGramCmd_SoundFileName,                      // 101

        DataGramCmd_RequestServerIP,    // request      // 102
        DataGramCmd_RcvdServerIP,        // answer      // 103

        // these are HLS Player cmds
        HlsCmd2Player_LoadPlayFile,                      // 104
        HlsCmd2Player_Mute,                              // 105
        HlsCmd2Player_Play,                              // 106
        HlsCmd2Player_Stop,                              // 107
        HlsCmd2Player_FullScreen,                        // 108
                                                         
        HlsCmd2Player_ShowLoadWithTime,                  // 109
        HlsCmd2Player_ShowPlayWithTime,                  // 110
        HlsCmd2Player_ShowLoadPlayFileDelay,             // 111

        HlsShowRemotePlay,                               // 112
        HlsShowRemotePause,                              // 113
        HlsShowRemoteStop,                               // 114
        HlsShowRemoteSelectSongPlay,                     // 115

        DataGramCmd_LastInList,
    };

    char* command_string[] = 
    {
        "DataGramCmd_LiveSound",
        "DataGramCmd_SoundFileName",
        "DataGramCmd_RequestServerIP",
        "DataGramCmd_RcvdServerIP",
        "HlsCmd2Player_LoadPlayFile",
        "HlsCmd2Player_Mute",            
        "HlsCmd2Player_Play",                
        "HlsCmd2Player_Stop",                    
        "HlsCmd2Player_FullScreen",
        "HlsCmd2Player_ShowLoadWithTime",
        "HlsCmd2Player_ShowPlayWithTime",        
        "HlsCmd2Player_ShowLoadPlayFileDelay",
        "HlsShowRemotePlay",
        "HlsShowRemotePause",                    
        "HlsShowRemoteStop",                
        "HlsShowRemoteSelectSongPlay",
        "DataGramCmd_LastInList"
    };

    struct udp_header
    {
        uint16_t absolute_size;
        command cmd;
        uint16_t data_size;
    };

    namespace endpoints
    {
        const uint16_t play_control     = 4311;
        const uint16_t server_handshake = 4357;
        const uint16_t local_handshake  = 4359;

        const char* broadcast_address = "255.255.255.255";
        const char* local_loopback = "127.0.0.1";
    };

    struct checksum
    {
        uint8_t data[3];
    };

    std::vector<uint8_t> create_udp_packet( command cmd, const char* data, size_t length )
    {
        size_t total_packet_size = sizeof(checksum) + sizeof(checksum) + sizeof(udp_header) + length;

        std::vector<uint8_t> packet( total_packet_size );
        
        checksum*   start_checksum = reinterpret_cast<checksum*>(   &(packet.data()[0]) );
        udp_header* header         = reinterpret_cast<udp_header*>( &(packet.data()[ sizeof(checksum) ]) );
        char*       packet_data    = reinterpret_cast<char*>(       &(packet.data()[ sizeof(checksum) + sizeof(udp_header) ]) );
        checksum*   end_checksum   = reinterpret_cast<checksum*>(   &(packet.data()[ sizeof(checksum) + sizeof(udp_header) + length]) );

        start_checksum->data[0] = 'J';
        start_checksum->data[1] = 'H';
        start_checksum->data[2] = 'S';

        header->absolute_size = total_packet_size;
        header->cmd = cmd;
        header->data_size = length;

        memcpy(packet_data, data, length);

        end_checksum->data[0] = 'J';
        end_checksum->data[1] = 'H';
        end_checksum->data[2] = 'E';

        return packet;
    }

    class invalid_packet_exception : public std::exception
    {
    };

    std::pair<udp_header*, char*> parse_udp_packet( boost::asio::mutable_buffer buffer )
    {
        checksum* start_checksum = boost::asio::buffer_cast<checksum*>(buffer);
        checksum* end_checksum = boost::asio::buffer_cast<checksum*>(buffer + (boost::asio::buffer_size(buffer) - 3) );

        if ( (start_checksum->data[0] != 'J') || (start_checksum->data[1] != 'H') || (start_checksum->data[2] != 'S') )
        {
            throw invalid_packet_exception();
        }

        if ( (end_checksum->data[0] != 'J') || (end_checksum->data[1] != 'H') || (end_checksum->data[2] != 'E') )
        {
            throw invalid_packet_exception();
        }

        std::pair<udp_header*, char*> header_data;
        header_data.first  = boost::asio::buffer_cast<udp_header*>(buffer + sizeof(checksum));
        header_data.second = boost::asio::buffer_cast<char*>(buffer + sizeof(checksum) + sizeof(udp_header) );

        return header_data;
    }

    const int max_buffer_size = 61000;
}

const int dataOffset = sizeof(hinkle::udp_header) + 3;

const int default_monitor = 0;
std::map<int, vlc_window*> vlc_monitors;

vlc_window* get_vlc_for_monitor(int monitor)
{
    auto mon = vlc_monitors.find(monitor);
    if (mon == vlc_monitors.end())
    {
        vlc_window* vlc = new vlc_window(monitor);
        vlc_monitors[monitor] = vlc;
        return vlc;
    }
    return mon->second;
}

void logerror(const char* format, ...)
{
    va_list args;
    va_start (args, format);
    vprintf (format, args);
    va_end (args);
}

class UdpListener
{
    boost::array<char, hinkle::max_buffer_size> recv_buf;
    boost::asio::io_service io_service;
    udp::socket socket;
    boost::asio::ip::udp::endpoint local_endpoint;

public:
    UdpListener() : socket(io_service, udp::endpoint(udp::v4(), 4311))
    {
        //io_service.
    }

    void run()
    {
        io_service.poll_one();
    }

    void handle_receive(
        const boost::system::error_code& error,
        size_t  size)
    {
        try
        {
            std::pair<hinkle::udp_header*, char*> packet = hinkle::parse_udp_packet( boost::asio::mutable_buffer(recv_buf.data(), size) );
            hinkle::udp_header* hdr = packet.first;

            printf("%d: Received %d \"%s\"\n", GetTickCount(), hdr->cmd, hinkle::command_string[ (int)hdr->cmd - 100 ] ) ;

            char* data = packet.second;
            data[hdr->data_size] = '\0';
            printf("%s\n", data);

            switch (hdr->cmd)
            {
                case hinkle::command::HlsCmd2Player_LoadPlayFile:
                {
                    auto vlc = get_vlc_for_monitor( default_monitor );
                    vlc->load(data);
                    vlc->play();
                }
                break;

                case hinkle::command::HlsCmd2Player_Stop:
                {
                    auto vlc = get_vlc_for_monitor( default_monitor );
                    vlc->pause();
                }
                break;

                case hinkle::command::HlsCmd2Player_Play:
                {
                    auto vlc = get_vlc_for_monitor( default_monitor );
                    vlc->play();
                }
                break;

                case hinkle::command::HlsCmd2Player_ShowLoadWithTime:
                {
                    std::cmatch m;
                    std::regex r("(.*),(.*),(.*)");
                    if (std::regex_match(data, m, r))
                    {
                        auto vlc = get_vlc_for_monitor( default_monitor );
                        vlc->load(m[3].str().c_str());
                    }
                }
                break;

                case hinkle::command::HlsCmd2Player_ShowPlayWithTime:
                {
                    auto vlc = get_vlc_for_monitor( default_monitor );
                    vlc->play();
                }
                break;
            }

            listen();
        }
        catch (hinkle::invalid_packet_exception& e)
        {
            logerror(e.what());
        }
    }

public:

    int listen()
    {
        try
        {
            socket.async_receive_from(boost::asio::buffer(recv_buf), local_endpoint,
                boost::bind(&UdpListener::handle_receive, this, boost::asio::placeholders::error, boost::asio::placeholders::bytes_transferred) );
        }
        catch (std::exception& e)
        {
            logerror(e.what());
        }
        return 0;
    }

    bool handshake()
    {
        boost::asio::io_service io_service;

        printf("Looking for HLS Server\n");

        // Set up a broadcast to 255.255.255.255, port 4357, and see who is out there.
        boost::asio::ip::udp::endpoint receiver_endpoint (boost::asio::ip::address::from_string(hinkle::endpoints::broadcast_address), hinkle::endpoints::server_handshake);
        udp::socket socket(io_service);

        // The broadcast needs to originate from port 4359.
        boost::asio::ip::udp::endpoint local_endpoint(boost::asio::ip::address::from_string(hinkle::endpoints::local_loopback), hinkle::endpoints::local_handshake);
        socket.open(udp::v4());
        boost::asio::socket_base::broadcast option(true);
        socket.set_option(option);
        socket.bind(local_endpoint);

        // Form the handshake packet 'JHS...payload...JHE'
        std::vector<uint8_t> packet = hinkle::create_udp_packet(hinkle::command::DataGramCmd_RequestServerIP, ">1", 2);

        socket.send_to(boost::asio::buffer(packet), receiver_endpoint);

        boost::array<char, hinkle::max_buffer_size> recv_buf2;
        udp::endpoint remote_endpoint;

        boost::system::error_code error;
        std::size_t size = socket.receive_from(boost::asio::buffer(recv_buf2), remote_endpoint, 0, error);
        printf("Found HLS Server\n");

        try
        {
            std::pair<hinkle::udp_header*, char*> packet = hinkle::parse_udp_packet( boost::asio::mutable_buffer(recv_buf2.data(), size) );
            printf("With valid response\n");
        }
        catch (hinkle::invalid_packet_exception& e)
        {
            logerror(e.what());
            return false;
        }

        return true;
    };
};

int main(int argc, char* argv[])
{
    try
    {
        UdpListener listener;

        while (!listener.handshake())
        {
        }

        //vlcwnd = new vlc_window(1, argc, argv);
        listener.listen();

        bool quit = false;

        while (!quit)
        {
            listener.run();

            for (auto v : vlc_monitors)
            {
                quit |= v.second->messageloop_step();
            }
        }
    }
    catch (std::exception& e)
    {
        logerror(e.what());
    }

    return 0;
}